To begin this chapter, allow me to provide a formal definition of the interface type. An interface is nothing more than a named set of abstract members. Recall from Chapter 6 that abstract methods are pure protocol in that they do not provide a default implementation. The specific members defined by an interface depend on the exact behavior it is modeling. Yes, it’s true. An interface expresses a behavior that a given class or structure may choose to support. Furthermore, as you will see in this chapter, a class (or structure) can support as many interfaces as necessary, thereby supporting (in essence) multiple behaviors.
As you might guess, the .NET base class libraries ship with hundreds of predefined interface types that are implemented by various classes and structures. For example, as you will see in Chapter 21, ADO.NET ships with multiple data providers that allow you to communicate with a particular database management system. Thus, under ADO.NET we have numerous connection objects to choose among (SqlConnection, OracleConnection, OdbcConnection, etc.).
Regardless of the fact that each connection object has a unique name, is defined within a different namespace, and (in some cases) is bundled within a different assembly, all connection objects implement a common interface named IDbConnection:
// The IDbConnection interface defines a common // set of members supported by all connection objects. public interface IDbConnection : IDisposable { // Methods IDbTransaction BeginTransaction(); IDbTransaction BeginTransaction(IsolationLevel il); void ChangeDatabase(string databaseName); void Close(); IDbCommand CreateCommand(); void Open(); // Properties string ConnectionString { get; set;} int ConnectionTimeout { get; } string Database { get; } ConnectionState State { get; } }
Note By convention, .NET interfaces are prefixed with a capital letter “I.” When you are creating your own custom interfaces, it is considered a best practice to do the same.
Don’t concern yourself with the details of what these members actually do at this point. Simply understand that the IDbConnection interface defines a set of members that are common to all ADO.NET connection objects. Given this, you are guaranteed that every connection object supports members such as Open(), Close(), CreateCommand(), and so forth. Furthermore, given that interface members are always abstract, each connection object is free to implement these methods in its own unique manner.
Another example: the System.Windows.Forms namespace defines a class named Control, which is a base class to a number of Windows Forms GUI widgets (DataGridView, Label, StatusBar, TreeView, etc.). The Control class implements an interface named IDropTarget, which defines basic drag-and-drop functionality:
public interface IDropTarget { // Methods void OnDragDrop(DragEventArgs e); void OnDragEnter(DragEventArgs e); void OnDragLeave(EventArgs e); void OnDragOver(DragEventArgs e); }
Based on this interface, you can correctly assume that any class that extends System.Windows.Forms.Control supports four methods named OnDragDrop(), OnDragEnter(), OnDragLeave(), and OnDragOver().
As you work through the remainder of this book, you’ll be exposed to dozens of interfaces that ship with the .NET base class libraries. As you will see, these interfaces can be implemented on your own custom classes and structures to define types that integrate tightly within the framework.
Given your work in Chapter 6, the interface type may seem very similar to an abstract base class. Recall that when a class is marked as abstract, it may define any number of abstract members to provide a polymorphic interface to all derived types. However, even when a class does define a set of abstract members, it is also free to define any number of constructors, field data, nonabstract members (with implementation), and so on. Interfaces, on the other hand, only contain abstract member definitions.
The polymorphic interface established by an abstract parent class suffers from one major limitation in that only derived types support the members defined by the abstract parent. However, in larger software systems, it is very common to develop multiple class hierarchies that have no common parent beyond System.Object. Given that abstract members in an abstract base class apply only to derived types, we have no way to configure types in different hierarchies to support the same polymorphic interface. By way of example, assume you have defined the following abstract class:
abstract class CloneableType { // Only derived types can support this // "polymorphic interface." Classes in other // hierarchies have no access to this abstract // member. public abstract object Clone(); }
Given this definition, only members that extend CloneableType are able to support the Clone() method. If you create a new set of classes that do not extend this base class, you can’t gain this polymorphic interface. Also you may recall, that C# does not support multiple inheritance for classes. Therefore, if you wanted to create a MiniVan that is-a Car and is-a CloneableType, you are unable to do so:
// Nope! Multiple inheritance is not possible in C# // for classes. public class MiniVan : Car, CloneableType { }
As you would guess, interface types come to the rescue. Once an interface has been defined, it can be implemented by any class or structure, in any hierarchy, within any namespace or any assembly (written in any .NET programming language). As you can see, interfaces are highly polymorphic. Consider the standard .NET interface named ICloneable defined in the System namespace. This interface defines a single method named Clone():
public interface ICloneable { object Clone(); }
If you examine the .NET Framework 4.0 SDK documentation, you’ll find that a large number of seemingly unrelated types (System.Array, System.Data.SqlClient.SqlConnection, System.OperatingSystem, System.String, etc.) all implement this interface. Although these types have no common parent (other than System.Object), we can treat them polymorphically via the ICloneable interface type.
For example, if you had a method named CloneMe() that took an ICloneable interface parameter, you could pass this method any object that implements said interface. Consider the following simple Program class defined within a Console Application named ICloneableExample:
class Program { static void Main(string[] args) { Console.WriteLine("***** A First Look at Interfaces *****\n"); // All of these classes support the ICloneable interface. string myStr = "Hello"; OperatingSystem unixOS = new OperatingSystem(PlatformID.Unix, new Version()); System.Data.SqlClient.SqlConnection sqlCnn = new System.Data.SqlClient.SqlConnection(); // Therefore, they can all be passed into a method taking ICloneable. CloneMe(myStr); CloneMe(unixOS); CloneMe(sqlCnn); Console.ReadLine(); } private static void CloneMe(ICloneable c) { // Clone whatever we get and print out the name. object theClone = c.Clone(); Console.WriteLine("Your clone is a: {0}", theClone.GetType().Name); } }
When you run this application, the class name of each class prints out to the console, via the GetType() method you inherit from System.Object (Chapter 15 provides full coverage of this method and .NET reflection services).
Source Code The ICloneableExample project is located under the Chapter 9 subdirectory.
Another limitation of traditional abstract base classes is that each derived type must contend with the set of abstract members and provide an implementation. To see this problem, recall the shapes hierarchy we defined in Chapter 6. Assume we defined a new abstract method in the Shape base class named GetNumberOfPoints(), which allows derived types to return the number of points required to render the shape:
{ ... // Every derived class must now support this method! public abstract byte GetNumberOfPoints(); }
Clearly, the only type that has any points in the first place is Hexagon. However, with this update, every derived class (Circle, Hexagon, and ThreeDCircle) must now provide a concrete implementation of this function even if it makes no sense to do so.
Again, the interface type provides a solution. If you define an interface that represents the behavior of “having points,” you can simply plug it into the Hexagon type, leaving Circle and ThreeDCircle untouched.